文章目录
  1. 1. Android 模板简介
    1. 1.1 Activity 模板的文件结构
    2. 1.2 代码生成流程
  2. 2. HTTemplate Activity 模板实现
    1. 2.1 template.xml
    2. 2.2 globals.xml.ftl
    3. 2.3 recipe.xml.ftl
    4. 2.4 root 文件夹
    5. 2.5 模板图标
    6. 2.6 使用 Activity 模板生成初始工程
  3. 3. 遇到的问题及解决方案
    1. 3.1 ${} 通配符冲突
    2. 3.2 Java 代码实例化问题
    3. 3.3 copy 和 instantiate 问题
      1. (1) gradle.properties 文件执行 copy 或者 instantiate 操作无效
      2. (2) copy 和 instantiate 对文件夹操作的区别
    4. 3.4 merge 问题
      1. (1) proguard-rules.pro、gradle.properties 文件执行 merge 操作失败
      2. (2) settings.gradle 文件合并,指定 module 路径错误
      3. (3) build.gradle 文件合并,apply 语句合并错误
      4. (4) build.gradle 文件合并,dependencies{ } 内的 apt 语句消失
  4. 4. 小结和后续工作

我们网易前端技术部 - 移动技术组作为公司的移动端基础技术部门,主要为其他部门提供解决方案、技术支持和产品孵化。在几年的积累过程中,我们拥有一些自己的框架和 SDK,如轻应用框架、热更新 SDK、网络请求库、本地存储库、页面管理等。服务过网易新闻、云音乐、考拉、易信等亿级产品,先后孵化过青果摄像头、二次元Gacha、严选等重要产品。

在多年的Android开发中,对于 Android 端产品开发,我们有如下几点体会:

  1. 产品孵化排期紧张

  2. 基础模块的需求具有相似性

  3. 基础模块的选型和工具类具有可重用性

  4. 网络请求的代码具有机械性

对于各个基础模块,我们团队封装了自己的 SDK,如网络库、本地存储库、页面管理库、图片库等。使用我们的Activity模板生成的初始工程,就已经包含了我们提供的基础模块,产品团队的开发不需要再花费重复的时间做技术调研、选型、SDK封装集成等工作,而只需要关心自己的业务逻辑编写。我们期望产品团队只需 1 分钟就能得到自己的初始工程,并能马上投入业务逻辑开发,既能缩短开发周期,也能保证工程代码质量。

1. Android 模板简介

Android Studio 提供遵循 Android 设计和开发最佳实践的代码模板,以帮助我们快速并正确地创造出漂亮的、功能齐全的应用程序代码模板。Android Studio 中提供的模板列表在不断增加,按照它们添加的组件类型(如Activity或XML文件)可对模板进行如下分组:

template menu

可通过文件->新建菜单或在项目窗口中右键单击调出上述模板菜单。

Android Studio 模板位置:

Windows 的路径在 ${android studio 安装路径}/plugins/android/lib/templates/

MacOS 的路径在 ${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/

该文件夹具体内容如下:

  • activities:Activity模板相关,如 EmptyActivity 文件夹用于创建一个空页面的模板,GoogleMapsActivity 文件夹对应创建一个地图页面的模板等

  • gradle:放置了 gradle 模板,用于在新建工程的根目录下生成 gradle 文件夹,支持用户不用安装 gradle 就能使用 gradlew 命令

  • gradle-project:工程模板相关,用于构建 module,Android Project,Java Library 等

  • other:构建文件模板等

模板最常见的用途之一是向现有应用程序模块添加新的 Activity。 activities 文件夹正是 Android Studio 默认提供的 Activity 模板,涵盖了手机和平板电脑应用中常用的 Activiy 模板。用户也可以参照已有模板自定义符合特定需求的 Activity 模板。

1.1 Activity 模板的文件结构

下面我们分析最简单的一个模板 EmptyActivity,我们首先查看下 EmtpyActivity (空白页面模板) 里面的内容

EmptyActivity Structrue

Android Studio 使用的是 FreeMarker 模板引擎,所以文件后缀都是 .ftl

  • globals.xml.ftl: 全局变量文件,保存一些全局变量,当中可以引用其他文件的全局变量

  • recipe.xml.ftl: 配置要引用的模板路径以及文件的生成规则

  • template.xml: 模板的配置信息,包括模板的显示图标,界面的表现,全局变量文件和执行文件的指定等

  • template_blank_activity.png: 显示的缩略图

  • SimpleActivity.java.ftl: Activity 模板文件

1.2 代码生成流程

目前我们已经基本了解了一个Activity模板的文件结构了,以及每个文件大致包含的东西,简单总结如下:

  • template 中parameter标签,主要用于提供参数

  • global.xml.ftl 主要用于提供参数

  • recipe.xml.ftl 主要用于生成我们实际需要的代码,资源文件等

    例如,利用参数 + MainActivity.java.ftl -> MainActivity.java,其实就是利用参数将ftl中的变量进行替换

代码生成过程如下图所示:

13_code_generation_process.jpg

图片摘自 Tutorial How To Create Custom Android Code Templates

2. HTTemplate Activity 模板实现

我们编写一个Activity模板叫作:HTTemplate,内容如下:

HTTemplate Structrue

2.1 template.xml

指定模板名、描述、最低支持 sdk 版本、类别等,输入界面要求指定包名和 Application 类名

2.2 globals.xml.ftl

引用公共文件内容

1
2
3
4
5
<?xml version="1.0"?>
<globals>
<global id="hasNoActionBar" type="boolean" value="false" />
<#include "../common/common_globals.xml.ftl" />
</globals>

2.3 recipe.xml.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0"?>
<recipe>
<!-- nei.json -->
<instantiate from="root/nei.json.ftl"
to="${escapeXmlAttribute(topOut)}/nei.json" />
<!-- manifest -->
<merge from="root/AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<merge from="root/AndroidManifestPermissions.xml"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<!-- 全部资源 -->
<copy from="root/res"
to="${escapeXmlAttribute(resOut)}" />
<!-- libs 库 -->
<copy from="root/libs"
to="${escapeXmlAttribute(projectOut)}/libs" />
<!-- gradle -->
<merge from="root/project_build.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/build.gradle" />
<merge from="root/app_build.gradle.ftl"
to="${escapeXmlAttribute(projectOut)}/build.gradle" />
<!-- README -->
<instantiate from="root/README.md.ftl"
to="${escapeXmlAttribute(topOut)}/README.md" />
<!-- proguard-rules.pro.ftl -->
<copy from="root/proguard-rules.pro.ftl"
to="${escapeXmlAttribute(projectOut)}/proguard-rules.pro.template" />
<!-- java 代码 -->
<!-- application 文件夹 -->
<instantiate from="root/src/app_package/application/AppProfile.java.ftl"
to="${escapeXmlAttribute(srcOut)}/application/AppProfile.java" />
...
<!-- attrs.xml -->
<merge from="root/res/values/attrs.xml"
to="${escapeXmlAttribute(resOut)}/values/attrs.xml" />
...
</recipe>

省略部分代码,主要的工作是

  • merge AndroidManifest.xml 文件

  • copy 或者 merge 资源文件

  • copy 或 instantiate java 代码

  • merge build.gradle 文件

  • merge settings.gradle 文件

  • copy lib 文件夹里面的全部内容

  • copy proguard-rules.pro 文件

2.4 root 文件夹

root Structure

放置相关模板源文件,包括一些Activity、自定义View、通用工具类等。将其中以.ftl为后缀的源代码,按照 FreeMarker 语法进行替换。例如,使用了包名的地方,需要替换成 ${packageName}

1
2
package ${packageName}.application;
import ${packageName}.R;

在gradle文件中配置私有maven库的地址、增加公用的依赖库、关闭lint的严格检查、配置APK多渠道打包等。

2.5 模板图标

添加 Activity 模板图标,并在 template.xml 中添加引用

1
2
3
<thumbs>
<thumb>template_thumb.png</thumb>
</thumbs>

2.6 使用 Activity 模板生成初始工程

将上述 HTTemplate 文件夹拷贝至 Android Studio 的 Activity 模板目录下:

Windows 的路径在

1
${android studio 安装路径}/plugins/android/lib/templates/activities

MacOS 的路径在

1
${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/activities

重启 Android Studio,在新建工程过程中可以看到,出现了我们自定义的 Activity 模板项:

新建项目流程之选择Activity

直接运行生成的项目,效果如下:

工程运行效果图

到这里,使用我们的 Activity 模板生成的初始工程,就已经包含了我们提供的网络库、本地存储库、页面管理库、图片库等基础模块,开发人员接下来只需专注于业务逻辑的开发。

3. 遇到的问题及解决方案

为分析问题的原因,我们找到了 Android 模板相关源码:

Mac 平台:

1
${android studio安装路径}/Contents/plugins/android/lib/android.jar

Windows 平台:

1
${android studio安装路径}/plugins/android/lib/android.jar

以下问题的解答将涉及到部分源码。

3.1 ${} 通配符冲突

当工程模板实例化时,${} 会被 FreeMarker 语法处理,导致错误。

解决办法:定义 FreeMarker 转义字符如下

1
$ ==> ${"$"}

3.2 Java 代码实例化问题

模板中 java 代码较多,我们统一放在 root/src/ 文件夹下,里面有部分文件含有 FreeMarker 标签,有部分只是纯粹的 java 代码。而使用 instantiate 命令对整个文件夹进行实例化操作,并不会触发 FreeMarker 语法执行。

解决办法:因 java 文件比较多,手写 recipe.xml 标签命令繁琐且容易出错。我们通过程序递归遍历 root/src/ 下的全部代码文件,并生成相应的 instantiate 或 copy 命令。

3.3 copy 和 instantiate 问题

(1) gradle.properties 文件执行 copy 或者 instantiate 操作无效

分析结果:查看 DefaultRecipeExecutor.copy 与 DefaultRecipeExecutor.instantiate 源码处理逻辑,得知执行 copy 和 instantiate 命令时,如果 from 指定一个非文件夹,且目标文件存在,则不执行拷贝。而在执行我们的 Activity 模板之前,已经执行了 gradle-projects/NewAndroidProject 工程模板,并生成了 gradle.properties 文件,因此执行 copy 或 instantiate 都因目标文件已经存在而不再执行。

(2) copy 和 instantiate 对文件夹操作的区别

  • 相同点:如果 from 指定一个文件夹,都是执行 copyTemplateResource 方法,二者没有区别;如果 from 指定一个非文件夹,且目标文件存在,则不执行文件操作。

  • 不同点:copy 命令不使用 FreemarkerUtils 对 FreeMarker 语法进行处理,而 instantiate 命令先执行 FreemarkerUtils 的静态方法 processFreemarkerTemplate 来处理 FreeMarker 语法,之后再执行文件拷贝操作。

3.4 merge 问题

(1) proguard-rules.pro、gradle.properties 文件执行 merge 操作失败

分析结果:根据 DefaultRecipeExecutor.merge 源码的逻辑,我们得知当 to 文件不存在,则执行 copy 或 instantiate 命令;如果 to 文件存在且可读,则仅对 xml 或 gradle 才能执行 merge 操作。

解决办法:

  • 暂时生成 proguard-rules.pro.template 文件

  • 将定义在 gradle.properties 中的常量移动到 project_build.gradle.ftl 的 ext{ } 内

(2) settings.gradle 文件合并,指定 module 路径错误

执行前:

1
2
3
4
5
6
include ':hteventbus', ':htrefreshrecyclerview', ':htrecycleview', ':hthttp'
project(':hteventbus').projectDir = new File('module/hteventbus')
project(':hthttp').projectDir = new File('module/hthttp')
project(':htrefreshrecyclerview').projectDir = new File('module/htrefreshrecyclerview')
project(':htrecycleview').projectDir = new File('module/htrecycleview')

执行后报错:

1
RuntimeException: java.lang.RuntimeException: When merging settings.gradle files, only include directives can be merged.

分析结果:查看 RecipeMergeUtils.mergeGradleSettingsFile 源码,得知当 settings.gradle 文件合并时,只允许每行开头是 include 命令,其他情况抛出异常。

解决办法:去掉非 include 的操作代码,改用远程依赖引用这些 module,即在 dependencies{ } 中添加相应的依赖。

(3) build.gradle 文件合并,apply 语句合并错误

执行前:

1
apply plugin: 'com.neenbedankt.android-apt'

执行后:

1
apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'

分析结果:查看 GradleFileMerger 中的 mergeGradleFiles 方法,实际执行的是 mergePsi 方法,根据 mergePsi 合并逻辑,apply 不是 call 语句,且 apply 的第一个子元素不是 dependencies,因此添加 plugin: ‘com.neenbedankt.android-apt’ 到 toRoot 的 apply 子元素前面。

解决办法:根据上面的分析,看起来 apply 的这个合成结果是 Android 模板的 bug,我们目前只能采用手工添加 apply 语句的方法。

(4) build.gradle 文件合并,dependencies{ } 内的 apt 语句消失

执行前:

1
2
3
4
5
dependencies {
compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
apt "com.netease.hearttouch:ht-universalrouter-dispatch-process:$HEARTTOUCH_HTROUTER_DISPATCH_PROCESS_VERSION"
...
}

执行后:

1
2
3
4
dependencies {
compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
...
}

分析结果:查看 GradleFileMerger.mergeDependencies 源码,得知当 dependencies 合并时,仅处理 dependencies 中的 compile 子元素,其他如 apt、provided 命令都会被忽略掉。

解决办法:由于源码并未提供非 compile 子元素的合并方案,我们目前只能采用手工添加 apt 语句的方法。

4. 小结和后续工作

到此,基本上完成了我们原先期望实现的初始工程:

  1. 提供 ht-template 支持生成我们的模板工程

  2. 提供 Android Studio 插件 (NEIPlugin)

  • 支持 ht-template 的下载安装

  • nei-toolkit 和 Node.js 的下载安装

  • nei-toolkit 和 Node.js 的使用,生成网络请求代码

这里还是有一些因为 Android 模板自身的限制而无法完成的内容点:

  1. 无法在 settings.gradle 指定 module 路径

  2. 无法合并 proguard-rules.pro 文件,暂时生成 proguard-rules.pro.template 文件

  3. 由于 build.gradle 对 apply 命令合并会出错和无法合并 dependencies 中的 apt 命令,所以无法在 build.gradle 中集成 ht-universalrouter

此外,除了网络请求的代码编写是机械性的,基于我们的 Activity 模板生成的初始工程,在其他方面也存在代码编写的机械性:初始页面代码生成、RecycleView 中的各个 ViewHolder 类、本地数据读取保存等,而这些工作将会是我们的后续工作。